/**
 * The utillib library.
 * More information is available at http://www.jinchess.com/.
 * Copyright (C) 2002 Alexander Maryanovsky.
 * All rights reserved.
 *
 * The utillib library is free software; you can redistribute
 * it and/or modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * The utillib library is distributed in the hope that it will
 * be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
 * General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with utillib library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package free.util;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Hashtable;
import java.util.Properties;


/**
 * Various utility methods that have something to do with I/O.
 */

public class IOUtilities {


    /**
     * Maps URLs to byte arrays of the data loaded from them.
     */

    private final static Hashtable urlCache = new Hashtable();


    /**
     * Returns a DataOutputStream object based on the given OutputStream.
     * If the given OutputStream is already an instance of DataOutputStream,
     * the same (given) OutputStream is casted to DataOutputStream and returned,
     * otherwise, a new wrapper DataOutputStream is created and returned.
     */

    public static DataOutputStream maybeCreateDataOutputStream(OutputStream out) {
        if (out instanceof DataOutputStream)
            return (DataOutputStream) out;
        else
            return new DataOutputStream(out);
    }


    /**
     * Returns a DataInputStream object based on the given InputStream.
     * If the given InputStream is already an instance of DataInputStream,
     * the same (given) InputStream is casted to DataInputStream and returned,
     * otherwise, a new wrapper DataInputStream is created and returned.
     */

    public static DataInputStream maybeCreateDataInputStream(InputStream in) {
        if (in instanceof DataInputStream)
            return (DataInputStream) in;
        else
            return new DataInputStream(in);
    }


    /**
     * Copies all the files of the given source directory into the given
     * destination directory, optionally recursively.
     */

    public static void copyDir(File source, File destination, boolean recurse) throws IOException {
        if (!source.exists())
            throw new IllegalArgumentException("The source directory (" + source + ") doesn't exist");
        if (!source.isDirectory())
            throw new IllegalArgumentException("The source (" + source + ") is a file, not a directory");
        if (!destination.exists())
            throw new IllegalArgumentException("The destination directory (" + destination + ") doesn't exist");
        if (!destination.isDirectory())
            throw new IllegalArgumentException("The destination (" + destination + ") is a file, not a directory");

        String[] filenames = source.list();
        for (int i = 0; i < filenames.length; i++) {
            String filename = filenames[i];
            File file = new File(source, filename);
            if (file.isDirectory()) {
                if (recurse) {
                    File destSubDir = new File(destination, filename);
                    if (!destSubDir.exists())
                        if (!destSubDir.mkdirs())
                            throw new IOException("Unable to create directory " + destSubDir);

                    copyDir(file, destSubDir, true);
                }
            } else {
                InputStream in = null;
                OutputStream out = null;
                try {
                    in = new FileInputStream(file);
                    out = new FileOutputStream(new File(destination, filename));
                    pump(in, out);
                } finally {
                    if (in != null)
                        in.close();
                    if (out != null)
                        out.close();
                }

            }
        }
    }


    /**
     * Removes the given directory and all files within it, recursively. Returns
     * <code>true</code> if successful, <code>false</code> otherwise. Note that if
     * it return <code>false</code>, some (or all) the files in the directory may
     * already be deleted.
     */

    public static boolean rmdir(File dir) {
        if (!dir.isDirectory())
            throw new IllegalArgumentException();

        String[] filenames = dir.list();
        for (int i = 0; i < filenames.length; i++) {
            File file = new File(dir, filenames[i]);
            if (file.isDirectory()) {
                if (!rmdir(file))
                    return false;
            } else if (!file.delete())
                return false;
        }

        return dir.delete();
    }


    /**
     * Writes the bytes read from the given input stream into the given output
     * stream until the end of the input stream is reached. Returns the amount of
     * bytes actually read/written.
     */

    public static int pump(InputStream in, OutputStream out) throws IOException {
        return pump(in, out, new byte[2048]);
    }


    /**
     * Writes up to the given amount of bytes read from the given input stream
     * into the given output stream until the end of the input stream is reached.
     * Returns the amount of bytes actually read/written.
     */

    public static int pump(InputStream in, OutputStream out, int amount) throws IOException {
        return pump(in, out, amount, new byte[2048]);
    }


    /**
     * Writes the bytes read from the given input stream into the given output
     * stream until the end of the input stream is reached. Returns the amount of
     * bytes actually read/written. Uses the given byte array as the buffer.
     */

    public static int pump(InputStream in, OutputStream out, byte[] buf) throws IOException {
        if (buf.length == 0)
            throw new IllegalArgumentException("Cannot use a 0 length buffer");

        int count;
        int amountRead = 0;
        while ((count = in.read(buf)) != -1) {
            out.write(buf, 0, count);
            amountRead += count;
        }

        return amountRead;
    }


    /**
     * Writes up to the given amount of bytes read from the given input stream
     * into the given output stream until the end of the input stream is reached.
     * Returns the amount of bytes actually read/written. Uses the given byte array
     * as the buffer.
     */

    public static int pump(InputStream in, OutputStream out, int amount, byte[] buf) throws IOException {
        if (buf.length == 0)
            throw new IllegalArgumentException("Cannot use a 0 length buffer");

        int amountRead = 0;
        while (amount > 0) {
            int amountToRead = amount > buf.length ? buf.length : amount;
            int count = in.read(buf, 0, amountToRead);
            if (count == -1)
                break;

            out.write(buf, 0, count);
            amount -= count;
            amountRead += count;
        }

        return amountRead;
    }


    /**
     * Reads from the given InputStream until its end and returns a byte array
     * of the contents. The input stream is not <code>close</code>d by this
     * method.
     */

    public static byte[] readToEnd(InputStream in) throws IOException {
        byte[] buf = new byte[2048];

        int amountRead = 0;
        int count = 0;
        while ((count = in.read(buf, amountRead, buf.length - amountRead)) > 0) {
            amountRead += count;

            if (amountRead == buf.length) {
                byte[] oldBuf = buf;
                buf = new byte[oldBuf.length * 2];
                System.arraycopy(oldBuf, 0, buf, 0, amountRead);
            }
        }

        byte[] arr = new byte[amountRead];
        System.arraycopy(buf, 0, arr, 0, amountRead);
        return arr;
    }


    /**
     * Reads the specified amount of bytes from the specified input stream and
     * returns the resulting array. Throws an <code>EOFException</code> if the
     * stream ends before the specified amount of bytes is read.
     */

    public static byte[] read(InputStream in, int amount) throws IOException {
        ByteArrayOutputStream buf = new ByteArrayOutputStream(amount);
        if (pump(in, buf, amount) != amount)
            throw new EOFException();

        return buf.toByteArray();
    }


    /**
     * Loads and returns data from the specified URL.
     */

    public static byte[] load(URL url, boolean allowCache) throws IOException {
        InputStream in = inputStreamForURL(url, allowCache);
        try {
            return readToEnd(in);
        } finally {
            try {
                in.close();
            } catch (IOException e) {
            }
        }
    }


    /**
     * Reads all the information from the given InputStream and returns it as
     * plain text by using the default system encoding. Note that this method
     * doesn't close the given InputStream, that is left to the user.
     */

    public static String loadText(InputStream in) throws IOException {
        return new String(readToEnd(in));
    }


    /**
     * Loads the text from the given URL and returns it as a string.
     *
     * @throws IOException if the given URL does not exist or an I/O error occurs
     *                     while accessing it.
     */

    public static String loadText(URL url, boolean allowCache) throws IOException {
        return new String(load(url, allowCache));
    }


    /**
     * Loads the given text file from the local drive, converts it to a String and
     * returns the String.
     *
     * @throws IOException if the file does not exist or loading failed.
     */

    public static String loadTextFile(File file) throws IOException {
        if (!file.exists())
            throw new IOException("File does not exist");

        InputStream in = new FileInputStream(file);
        String text = loadText(in);
        in.close();
        return text;
    }


    /**
     * Loads a text file with the given name from the local drive, converts it to
     * a String and returns the String.
     *
     * @throws IOException if the file does not exist or loading failed.
     */

    public static String loadTextFile(String filename) throws IOException {
        return loadTextFile(new File(filename));
    }


    /**
     * Compares the 2 given sub arrays. Returns true if they are equal, false
     * otherwise.
     *
     * @throws ArrayIndexOutOfBounds if
     *                               <UL>
     *                               <LI> <code>offset1</code> or <code>offset2</code> are negative.
     *                               <LI> length is negative.
     *                               <LI> <code>offset1+length</code> is bigger than <code>arr1.length</code>
     *                               <LI> <code>offset2+length</code> is bigger than <code>arr2.length</code>
     *                               </UL>
     */

    public static boolean equal(byte[] arr1, int offset1, byte[] arr2, int offset2, int length) {
        if ((offset1 < 0) || (offset2 < 0) || (length < 0) || (offset1 + length > arr1.length) || (offset2 + length > arr2.length))
            throw new ArrayIndexOutOfBoundsException();

        for (int i = 0; i < length; i++) {
            if (arr1[offset1 + i] != arr2[offset2 + i])
                return false;
        }

        return true;
    }


    /**
     * Returns a <code>URL</code> corresponding to the specified <code>File</code>
     * or <code>null</code> if the <code>File</code> cannot be converted into a
     * <code>URL</code>.
     * NOTE: This is copied from the JDK1.3 source, File.java
     */

    public static URL fileToURL(File file) {
        try {
            String path = file.getAbsolutePath();
            if (File.separatorChar != '/')
                path = path.replace(File.separatorChar, '/');
            if (!path.startsWith("/"))
                path = "/" + path;
            if (!path.endsWith("/") && file.isDirectory())
                path = path + "/";
            return new URL("file", "", path);
        } catch (MalformedURLException e) {
            return null;
        }
    }


    /**
     * Creates and returns a new <code>java.util.Properties</code> object loaded
     * from the specified <code>InputStream</code>.
     */

    public static Properties loadProperties(InputStream in) throws IOException {
        return loadProperties(in, new Properties());
    }


    /**
     * Loads properties from the specified <code>InputStream</code> into the
     * specified <code>Properties</code> object. Returns the passed
     * <code>Properties</code> object.
     */

    public static Properties loadProperties(InputStream in, Properties props) throws IOException {
        if (in == null)
            return null;

        props.load(in);

        return props;
    }


    /**
     * Similar to the {@link #loadProperties(InputStream)} method, but closes
     * the specified <code>InputStream</code> at the end of its operation.
     */

    public static Properties loadPropertiesAndClose(InputStream in) throws IOException {
        return loadPropertiesAndClose(in, new Properties());
    }


    /**
     * Similar to the {@link #loadProperties(InputStream, Properties)} method,
     * but closes the specified <code>InputStream</code> at the end of its
     * operation.
     */

    public static Properties loadPropertiesAndClose(InputStream in, Properties props) throws IOException {
        try {
            return loadProperties(in, props);
        } finally {
            try {
                in.close();
            } catch (IOException e) {
            }
        }
    }


    /**
     * Creates and returns a new <code>java.util.Properties</code> object loaded
     * from the specified <code>File</code>.
     */

    public static Properties loadProperties(File file) throws IOException {
        return loadPropertiesAndClose(new FileInputStream(file));
    }


    /**
     * Creates and returns a new <code>java.util.Properties</code> object loaded
     * from the specified <code>URL</code>.
     * <code>allowCache</code> specifies whether the data may be retrieved from
     * the cache instead of being actually retrieved.
     */

    public static Properties loadProperties(URL url, boolean allowCache) throws IOException {
        return loadProperties(url, allowCache, new Properties());
    }


    /**
     * Loads properties from the specified <code>URL</code> into the specified
     * </code>Properties</code> object. Returns the passed
     * <code>Properties</code> object.
     * <code>allowCache</code> specifies whether the data may be retrieved from
     * the cache instead of being actually retrieved.
     */

    public static Properties loadProperties(URL url, boolean allowCache, Properties props) throws IOException {
        return loadPropertiesAndClose(inputStreamForURL(url, allowCache), props);
    }


    /**
     * Loads and caches the contents of the specified URL. Calls to any of the
     * methods that load from URLs in this class will use the cached data. Calling
     * this method with an already cached URL will cause it to be loaded again. If
     * an <code>IOException</code> occurs while loading the data, the cache
     * remains unchanged.
     */

    public static void cacheURL(URL url) throws IOException {
        cacheData(url, load(url, false));
    }


    /**
     * Forces the data mapped to the specified URL to be the specified data.
     * This method is useful when one part of an application wants to generate
     * or specify data for another part.
     */

    public static void cacheData(URL url, byte[] data) {
        urlCache.put(url, data);
    }


    /**
     * Returns whether the specified URL is cached.
     */

    public static boolean isURLCached(URL url) {
        return urlCache.containsKey(url);
    }


    /**
     * Returns an <code>InpuStream</code> for reading the data at the specified
     * URL. If <code>allowCache</code> is <code>true</code>, and the URL is cached,
     * a <code>ByteArrayInpuStream</code> with the cached data is returned.
     */

    public static InputStream inputStreamForURL(URL url, boolean allowCache) throws IOException {
        byte[] cached = null;
        if (allowCache)
            cached = (byte[]) urlCache.get(url);
        return cached == null ? url.openStream() : new ByteArrayInputStream(cached);
    }


    /**
     * Loads data from the specified URLs asynchronously in a background thread.
     * Once all the data is loaded, it is passed to the specified
     * <code>DataReceiver</code>. <code>id</code> is a convenience allowing the
     * receiver to identify the data - it is merely passed back to the receiver.
     */

    public static void loadAsynchronously(URL[] urls, Object id, DataReceiver receiver, boolean allowCache) {
        Thread asyncReader =
                new Thread(new UrlDataReader((URL[]) urls.clone(), id, receiver, allowCache),
                        "AsyncThread-" + (++UrlDataReader.threadCount));
        asyncReader.setDaemon(true);
        asyncReader.start();
    }


    /**
     * Similar to <code>loadAsynchronously</code>, but returns only when all the
     * data has been loaded and passed off to the receiver.
     */

    public static void loadSynchronously(URL[] urls, Object id, DataReceiver receiver, boolean allowCache) {
        new UrlDataReader((URL[]) urls.clone(), id, receiver, allowCache).run();
    }


    /**
     * The callback interface for asynchronous reading of data.
     */

    public static interface DataReceiver {


        /**
         * Gets called when all the data is loaded.
         * The <code>IOException</code> array holds the exceptions thrown while
         * loading. The indices in all the arrays correspond.
         */

        void dataRead(URL[] urls, Object id, byte[][] data, IOException[] exceptions);


    }


    /**
     * Reads data from URLs.
     */

    private static class UrlDataReader implements Runnable {


        /**
         * The number of <code>Threads</code> running <code>UrlDataReader</code>s
         * already created.
         */

        public static int threadCount = 0;


        /**
         * The URLs to load data from.
         */

        private final URL[] urls;


        /**
         * The identifier of this download.
         */

        private final Object id;


        /**
         * The callback <code>DataReceiver</code>.
         */

        private final DataReceiver receiver;


        /**
         * Whether it is allowed for the data to be retrieved from cache.
         */

        private final boolean allowCache;


        /**
         * The data.
         */

        private final byte[][] data;


        /**
         * The <code>IOExceptions</code> thrown while loading the data.
         */

        private final IOException[] exceptions;


        /**
         * Creates a new <code>UrlDataReader</code> with the specified id, to load
         * data from the specified URLs and report back to the specified
         * <code>DataReceiver</code>.
         */

        public UrlDataReader(URL[] urls, Object id, DataReceiver receiver, boolean allowCache) {
            this.urls = urls;
            this.id = id;
            this.receiver = receiver;
            this.allowCache = allowCache;
            this.data = new byte[urls.length][];
            this.exceptions = new IOException[urls.length];
        }


        /**
         * Reads the data and reports back to the receiver.
         */

        public void run() {
            for (int i = 0; i < urls.length; i++) {
                try {
                    data[i] = load(urls[i], allowCache);
                } catch (IOException e) {
                    exceptions[i] = e;
                }
            }

            receiver.dataRead(urls, id, data, exceptions);
        }


    }


}
